package org.dromara.common.redis.spring;

import cn.hutool.core.convert.Convert;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.data.redis.core.HashOperations;

import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Author: fly
 * Date: 2024/3/10
 **/
public class RedisTemplateHashCache implements Cache {

    private LockTemplate lockTemplate;

    private HashOperations cacheMap;

    private String name;

    private CacheConfig config;

    private final boolean allowNullValues;

    private final AtomicLong hits = new AtomicLong();

    private final AtomicLong puts = new AtomicLong();

    private final AtomicLong misses = new AtomicLong();

    private final AtomicLong evictions = new AtomicLong();

    public RedisTemplateHashCache(HashOperations cacheMap, String name, CacheConfig config, boolean allowNullValues) {
        this(cacheMap, name, allowNullValues);
        this.config = config;
    }

    public RedisTemplateHashCache(HashOperations cacheMap, String name, boolean allowNullValues) {
        this.name = name;
        this.cacheMap = cacheMap;
        this.allowNullValues = allowNullValues;
        this.lockTemplate = SpringUtils.getBean(LockTemplate.class);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Map<?, ?> getNativeCache() {
        return RedisUtils.getCacheMap(name);
    }

    @Override
    public Cache.ValueWrapper get(Object key) {
        Object value;
        value = cacheMap.get(name,  Convert.toStr(key));

        if (value == null) {
            addCacheMiss();
        } else {
            addCacheHit();
        }
        return toValueWrapper(value);
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        Object value;
        value = cacheMap.get(name, Convert.toStr(key));

        if (value == null) {
            addCacheMiss();
        } else {
            addCacheHit();
            if (value.getClass().getName().equals(NullValue.class.getName())) {
                return null;
            }
            if (type != null && !type.isInstance(value)) {
                throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
            }
        }
        return (T) fromStoreValue(value);
    }

    @Override
    public void put(Object key, Object value) {
        if (!allowNullValues && value == null) {
            cacheMap.delete(name, (String)  Convert.toStr(key));
            return;
        }

        value = toStoreValue(value);
        cacheMap.put(name, (String)  Convert.toStr(key), value);
        addCachePut();
    }

    @Override
    public Cache.ValueWrapper putIfAbsent(Object key, Object value) {
        Object prevValue;
        if (!allowNullValues && value == null) {
            prevValue = cacheMap.get(name,  Convert.toStr(key));
        } else {
            value = toStoreValue(value);
            prevValue = cacheMap.putIfAbsent(name, (String)  Convert.toStr(key), value);
            if (prevValue == null) {
                addCachePut();
            }
        }

        return toValueWrapper(prevValue);
    }

    @Override
    public void evict(Object key) {
        evictIfPresent(key);
    }

    @Override
    public boolean evictIfPresent(Object key) {
        long delta = cacheMap.delete(name, (String)  Convert.toStr(key));
        addCacheEvictions(delta);
        return delta > 0;
    }

    @Override
    public void clear() {
        cacheMap.delete(name, cacheMap.keys(name));
    }

    @Override
    public boolean invalidate() {
        cacheMap.delete(name, cacheMap.keys(name));
        return true;
    }

    private Cache.ValueWrapper toValueWrapper(Object value) {
        if (value == null) {
            return null;
        }
        if (value.getClass().getName().equals(NullValue.class.getName())) {
            return NullValue.INSTANCE;
        }
        return new SimpleValueWrapper(value);
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        Object value;
        value = cacheMap.get(name,  Convert.toStr(key));

        if (value == null) {
            addCacheMiss();
            LockInfo lock = lockTemplate.lock((String)  Convert.toStr(key));
            try {
                value = cacheMap.get(name,  Convert.toStr(key));
                if (value == null) {
                    value = putValue( key, valueLoader, value);
                }
            } finally {
                lockTemplate.releaseLock(lock);
            }
        } else {
            addCacheHit();
        }

        return (T) fromStoreValue(value);
    }

    private <T> Object putValue(Object key, Callable<T> valueLoader, Object value) {
        try {
            value = valueLoader.call();
        } catch (Exception ex) {
            RuntimeException exception;
            try {
                Class<?> c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException");
                Constructor<?> constructor = c.getConstructor(Object.class, Callable.class, Throwable.class);
                exception = (RuntimeException) constructor.newInstance(key, valueLoader, ex);
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
            throw exception;
        }
        put(key, value);
        return value;
    }

    protected Object fromStoreValue(Object storeValue) {
        if (storeValue instanceof NullValue) {
            return null;
        }
        return storeValue;
    }

    protected Object toStoreValue(Object userValue) {
        if (userValue == null) {
            return NullValue.INSTANCE;
        }
        return userValue;
    }

    /**
     * The number of get requests that were satisfied by the cache.
     *
     * @return the number of hits
     */
    long getCacheHits() {
        return hits.get();
    }

    /**
     * A miss is a get request that is not satisfied.
     *
     * @return the number of misses
     */
    long getCacheMisses() {
        return misses.get();
    }

    long getCachePuts() {
        return puts.get();
    }

    long getCacheEvictions() {
        return evictions.get();
    }

    private void addCachePut() {
        puts.incrementAndGet();
    }

    private void addCacheHit() {
        hits.incrementAndGet();
    }

    private void addCacheMiss() {
        misses.incrementAndGet();
    }

    private void addCacheEvictions(long delta) {
        evictions.addAndGet(delta);
    }
}
